/*
 * Created on Mar 14, 2005
 */
package org.flexdock.docking.defaults;

import java.awt.Component;
import java.awt.Container;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Map;

import javax.swing.JSplitPane;
import javax.swing.SwingUtilities;
import javax.swing.plaf.basic.BasicSplitPaneDivider;
import javax.swing.plaf.basic.BasicSplitPaneUI;

import org.flexdock.docking.Dockable;
import org.flexdock.docking.DockingConstants;
import org.flexdock.docking.DockingManager;
import org.flexdock.docking.DockingPort;
import org.flexdock.docking.DockingStrategy;
import org.flexdock.docking.RegionChecker;
import org.flexdock.docking.defaults.DockingSplitPane;
import org.flexdock.docking.drag.DragManager;
import org.flexdock.docking.drag.DragOperation;
import org.flexdock.docking.event.DockingEvent;
import org.flexdock.docking.floating.frames.DockingFrame;
import org.flexdock.docking.floating.frames.FloatingDockingPort;
import org.flexdock.docking.props.DockablePropertySet;
import org.flexdock.docking.state.FloatManager;
import org.flexdock.event.EventManager;
import org.flexdock.util.DockingUtility;
import org.flexdock.util.RootWindow;
import org.flexdock.util.SwingUtility;

/**
 * @author Christopher Butler
 */
public class DefaultDockingStrategy implements DockingStrategy,
		DockingConstants {

	public static final String PREFERRED_PROPORTION = "DefaultDockingStrategy.PREFERRED_PROPORTION";

	private static double defaultResizeWeight = -1;
	private static boolean constantPercent;

	/**
	 * Returns the specified {@code Dockable's} sibling {@code Dockable} within
	 * the current docking layout. This method checks the parent
	 * {@code DockingPort} of a given {@code Dockable} to see if it is split
	 * equally with another {@code Dockable}. If so, the immediate sibling
	 * {@code Dockable} is returned. If there are more than two
	 * {@code Dockables} within the split layout, then the closest sibling
	 * region is determined and this method dispatches to
	 * {@code getSibling(Dockable dockable, String region)}.
	 * <p>
	 * If the specified {@code Dockable} is {@code null}, or there are no
	 * siblings available in the docking layout, then this methdo returns a
	 * {@code null} reference. If the specified {@code Dockable} is not
	 * currently docked within a {@code DockingPort}, then this method returns
	 * a {@code null} reference.
	 *
	 * @param dockable the {@code Dockable} whose sibling is to be returned
	 * @return the sibling of the specified {@code Dockable} within the current
	 * docking layout.
	 * @see Dockable#getDockingPort()
	 * @see #getSibling(Dockable, String)
	 */
	public static Dockable getSibling(Dockable dockable) {
		if (dockable == null)
			return null;

		DockingPort port = dockable.getDockingPort();
		String startRegion = findRegion(dockable.getComponent());
		String region = DockingUtility.flipRegion(startRegion);
		Dockable sibling = findDockable(port, dockable.getComponent(), region,
				startRegion);

		return sibling;
	}

	/**
	 * Returns the sibling {@code Dockable} relative to the specified
	 * {@code Dockable's} supplied region in the current docking layout. If
	 * {@code dockable} is {@code null} or {@code region} is either invalid or
	 * equal to {@code CENTER_REGION}, then this method returns a {@code null}
	 * reference.
	 * <p>
	 * If the specified {@code Dockable} is in a {@code DockingPort} that
	 * equally splits the layout between two {@code Dockables} in a fashion that
	 * matches up with the specified region, then the immediate sibling
	 * {@code Dockable} is returned. Otherwise, a fuzzy search is performed
	 * throughout the docking layout for a {@code Dockable} that "looks like" it
	 * is docked to the supplied region of the specified {@code Dockable} from a
	 * visual standpoint.
	 * <p>
	 * For instance, a docking layout may consist of four quadrants <i>Dockable1</i>
	 * (top-left), <i>Dockable2</i> (top-right), <i>Dockable3</i>
	 * (bottom-left) and <i>Dockable4</i> (bottom-right). The layout is built
	 * by docking <i>Dockable2>/i> to the {@code EAST_REGION} of <i>Dockable1</i>,
	 * <i>Dockable3</i> to the {@code SOUTH_REGION} of <i>Dockable1</i>, and
	 * <i>Dockable4</i> to the {@code SOUTH_REGION} of <i>Dockable2</i>.
	 * Within this layout, <i>Dockable1</i> and <i>Dockable3</i> are immediate
	 * siblings, as are <i>Dockable2</i> and <i>Dockable4</i>. Thus,
	 * requesting sibling NORTH_REGION of <i>Dockable3</i> will easily yield
	 * <i>Dockable1</i>. However, <i>Dockable3</i> has no immediate
	 * {@code EAST_REGION} sibling. In this case, a fuzzy search through the
	 * layout is performed to determine the visual sibling, and this method
	 * returns <i>Dockable4</i>. Likewise, this method will return a
	 * {@code null} reference for the {@code WEST_REGION} sibling of
	 * <i>Dockable3}, since there are no {@code Dockables} in the visual layout
	 * to the west of this {@code Dockable}.
	 *
	 * @param dockable the {@code Dockable} whose sibling is to be returned
	 * @param region   the region of the specified {@code Dockable} whose visual
	 *                 sibling is to be returned
	 * @return the {@code Dockable} in the supplied region relative to the
	 * specified {@code Dockable}
	 */
	public static Dockable getSibling(Dockable dockable, String region) {
		if (dockable == null || !DockingManager.isValidDockingRegion(region)
				|| CENTER_REGION.equals(region))
			return null;

		DockingPort port = dockable.getDockingPort();
		String startRegion = findRegion(dockable.getComponent());
		Dockable sibling = findDockable(port, dockable.getComponent(), region,
				startRegion);

		return sibling;
	}

	private static Dockable findDockable(DockingPort port, Component self,
	                                     String region, String startRegion) {
		if (port == null)
			return null;

		Component docked = port.getDockedComponent();
		// if we're not a split port, then there is no concept of 'outer
		// regions'.
		// jump up a level to find the parent split port
		if (!(docked instanceof JSplitPane)) {
			DockingPort superPort = DockingManager
					.getDockingPort((Component) port);
			return findDockable(superPort, self, region, startRegion);
		}

		Component sibling = port.getComponent(region);
		if (sibling == self) {
			if (!(self instanceof JSplitPane)) {
				DockingPort superPort = DockingManager
						.getDockingPort((Component) port);

				return findDockable(superPort, self, region, startRegion);
			}
			return null;
		}

		if (sibling instanceof JSplitPane) {
			// go one level deeper
			DockingPort subPort = DockingManager.getDockingPort(sibling);
			Component other = port.getComponent(DockingUtility
					.flipRegion(region));
			String subRegion = findSubRegion((JSplitPane) sibling, other,
					region, startRegion);
			return findDockable(subPort, self, subRegion, startRegion);
		}

		// if we have no direct sibling in the specified region, the jump
		// up a level.
		if (sibling == null) {
			DockingPort superPort = DockingManager
					.getDockingPort((Component) port);
			self = port.getDockedComponent();
			return findDockable(superPort, self, region, startRegion);
		}

		return DockingManager.getDockable(sibling);
	}

	private static String findSubRegion(JSplitPane split, Component other,
	                                    String targetRegion, String baseRegion) {
		String region = DockingUtility.translateRegionAxis(split, targetRegion);
		if (!(other instanceof JSplitPane))
			return region;

		boolean translated = !targetRegion.equals(region);
		if (translated && !DockingUtility.isAxisEquivalent(region, baseRegion)) {
			region = DockingUtility.flipRegion(region);
		}

		return region;
	}

	/**
	 * Returns the docking region within the current split docking layout
	 * containing the specified {@code Component}. If {@code comp} is
	 * {@code null}, then a {@code null} reference is returned. If {@code comp}
	 * is not in a split layout, then {@code CENTER_REGION} is returned.
	 * <p>
	 * This method resolves the associated {@code Dockable} and
	 * {@code DockingPort} for the specified {@code Component} and backtracks
	 * through the docking layout to find a split layout. If a split layout is
	 * found, then the region retured by this method is calculated relative to
	 * its sibling in the layout.
	 *
	 * @param comp the {@code Component} whose region is to be returned
	 * @return the region of the current split layout containing the specified
	 * {@code Dockable}
	 */
	public static String findRegion(Component comp) {
		if (comp == null)
			return null;

		DockingPort port = DockingManager.getDockingPort(comp);
		Component docked = port.getDockedComponent();

		if (!(docked instanceof JSplitPane)) {
			// we didn't find a split pane, to check the grandparent dockingport
			DockingPort superPort = DockingManager
					.getDockingPort((Component) port);
			// if there was no grandparent DockingPort, then we're stuck with
			// the docked
			// component we already found. this can happen on the root
			// dockingport.
			docked = superPort == null ? docked : superPort
					.getDockedComponent();
		}

		if (!(docked instanceof JSplitPane))
			return CENTER_REGION;

		JSplitPane split = (JSplitPane) docked;
		boolean horiz = split.getOrientation() == JSplitPane.HORIZONTAL_SPLIT;
		Component left = split.getLeftComponent();
		if (left == port) {
			return horiz ? WEST_REGION : NORTH_REGION;
		}
		return horiz ? EAST_REGION : SOUTH_REGION;

	}

	/**
	 * Docks the specified {@code Dockable} into the supplied {@code region} of
	 * the specified {@code DockingPort}. This method is meant for programmatic
	 * docking, as opposed to realtime, event-based docking operations. As such,
	 * it defers processing to
	 * {@code dock(Dockable dockable, DockingPort port, String region, DragOperation token)},
	 * passing a {@code null} argument for the {@code DragOperation} parameter.
	 * This implies that there is no event-based drag operation currently in
	 * progress to control the semantics of the docking operation, only that an
	 * attempt should be made to dock the specified {@code Dockable} into the
	 * specified {@code DockingPort}.
	 * <p>
	 * This method will return {@code false} if {@code dockable} or {@code port}
	 * are {@code null}, or if {@code region} is not a valid region according
	 * to the specified {@code DockingPort}. If a {@code Dockable} is currently
	 * docked within the specified {@code DockingPort}, then that
	 * {@code Dockable's} territorial properties are also checked and this
	 * method may return {@code false} if the territory is blocked. Finally,
	 * this method will return {@code false} if the specified {@code Dockable}
	 * is already docked within the supplied region of the specified
	 * <code.DockingPort}.
	 *
	 * @param dockable the {@code Dockable} we wish to dock
	 * @param port     the {@code DockingPort} into which we wish to dock
	 * @param region   the region of the specified {@code DockingPort} into which we
	 *                 wish to dock.
	 * @return {@code true} if the docking operation was successful,
	 * {@code false}. otherwise.
	 * @see #dock(Dockable, DockingPort, String, DragOperation)
	 * @see Dockable#getDockingProperties()
	 * @see DockablePropertySet#isTerritoryBlocked(String)
	 */
	public boolean dock(Dockable dockable, DockingPort port, String region) {
		return dock(dockable, port, region, null);
	}

	/**
	 * Docks the specified {@code Dockable} into the supplied {@code region} of
	 * the specified {@code DockingPort}. This method is meant for realtime,
	 * event-based docking based on an in-progress drag operation. It is not
	 * recommended for developers to call this method programmatically, except
	 * to pass in a {@code null} {@code DragOperation} argument. *
	 * <p>
	 * The {@code DragOperation} parameter, if present, will control the
	 * semantics of the docking operation based upon current mouse position,
	 * drag threshold, and a customizable drag context {@code Map}. For
	 * instance, the {@code DragOperation} may contain information regarding the
	 * {@code Dockable} over which the mouse is currently hovered, whether the
	 * user is attempting to drag a {@code Dockable} outside the bounds of any
	 * existing windows (perhaps in an attempt to float the {@code Dockable}),
	 * or whether the current distance offset from the original drag point
	 * sufficiently warrants a valid docking operation.
	 * <p>
	 * If the {@code DragOperation} is {@code null}, then this method will
	 * attempt to programmatically dock the specified {@code Dockable} into the
	 * supplied {@code region} of the specified {@code DockingPort} without
	 * regard to external event-based criteria. This is in accordance with the
	 * behavior specified by
	 * {@code dock(Dockable dockable, DockingPort port, String region)}.
	 * <p>
	 * This method will return {@code false} if {@code dockable} or {@code port}
	 * are {@code null}, or if {@code region} is not a valid region according
	 * to the specified {@code DockingPort}. If a {@code Dockable} is currently
	 * docked within the specified {@code DockingPort}, then that
	 * {@code Dockable's} territorial properties are also checked and this
	 * method may return {@code false} if the territory is blocked. If a
	 * {@code DragOperation} is present, then this method will return
	 * {@code false} if the required drag threshold has not been exceeded.
	 * Finally, this method will return {@code false} if the specified
	 * {@code Dockable} is already docked within the supplied region of the
	 * specified <code.DockingPort}.
	 *
	 * @param dockable the {@code Dockable} we wish to dock
	 * @param port     the {@code DockingPort} into which we wish to dock
	 * @param region   the region of the specified {@code DockingPort} into which we
	 *                 wish to dock.
	 * @return {@code true} if the docking operation was successful,
	 * {@code false}. otherwise.
	 * @see #dock(Dockable, DockingPort, String, DragOperation)
	 * @see Dockable#getDockingProperties()
	 * @see DockablePropertySet#isTerritoryBlocked(String)
	 */
	public boolean dock(Dockable dockable, DockingPort port, String region,
	                    DragOperation operation) {
		if (!isDockingPossible(dockable, port, region, operation))
			return false;

		if (!dragThresholdElapsed(operation))
			return false;

		// cache the old parent
		DockingPort oldPort = dockable.getDockingPort();

		// perform the drop operation.
		DockingResults results = dropComponent(dockable, port, region,
				operation);

		// perform post-drag operations
		DockingPort newPort = results.dropTarget;
		int evtType = results.success ? DockingEvent.DOCKING_COMPLETE
				: DockingEvent.DOCKING_CANCELED;
		Map dragContext = DragManager.getDragContext(dockable);
		DockingEvent evt = new DockingEvent(dockable, oldPort, newPort,
				evtType, dragContext);
		// populate DockingEvent status info
		evt.setRegion(region);
		evt.setOverWindow(operation == null ? true : operation.isOverWindow());

		// notify the old docking port, new dockingport,and dockable
		Object[] evtTargets = {oldPort, newPort, dockable};
		EventManager.dispatch(evt, evtTargets);

		return results.success;
	}

	protected boolean dragThresholdElapsed(DragOperation token) {
		if (token == null || token.isPseudoDrag() || token.getStartTime() == -1)
			return true;

		long elapsed = System.currentTimeMillis() - token.getStartTime();
		// make sure the elapsed time of the drag is at least over .2 seconds.
		// otherwise, we'll probably be responding to inadvertent clicks (maybe
		// double-clicks)
		return elapsed > 200;
	}

	protected boolean isDockingPossible(Dockable dockable, DockingPort port,
	                                    String region, DragOperation token) {
		// superclass blocks docking if the 'port' or 'region' are null. If
		// we've dragged outside
		// the bounds of the parent frame, then both of these will be null. This
		// is expected here and
		// we intend to float in this case.
		if (isFloatable(dockable, token))
			return true;

		// check to see if we're already floating and we're trying to drop into
		// the
		// same dialog.
		DockingPort oldPort = DockingManager.getDockingPort(dockable);
		if (oldPort instanceof FloatingDockingPort && oldPort == port) {
			// only allow this situation if we're not the *last* dockable
			// in the viewport. if we're removing the last dockable, then
			// the dialog will disappear before we redock, and we don't want
			// this
			// to happen.
			FloatingDockingPort floatingDockingPort = (FloatingDockingPort) oldPort;
			if (floatingDockingPort.getDockableCount() == 1)
				return false;
		}

		if (dockable == null || dockable.getComponent() == null || port == null)
			return false;

		if (!DockingManager.isValidDockingRegion(region))
			return false;

		Dockable docked = DockingManager.getDockable(port.getDockedComponent());
		if (docked == null)
			return true;

		// don't allow them to dock into this region if the territory there is
		// blocked.
		if (docked.getDockingProperties().isTerritoryBlocked(region)
				.booleanValue())
			return false;

		// check to see if we're already docked into this region.
		// get the parent dockingPort.
		Container container = docked.getComponent().getParent();
		// now get the grandparent dockingport
		DockingPort grandparent = DockingManager.getDockingPort(container);

		// if we don't share the grandparent dockingport, then we're definitely
		// not split in the same dockingport
		// across different region. in this case, it's ok to proceed with the
		// dock
		if (grandparent == null)
			return true;

		Component currentlyInRegion = grandparent.getComponent(region);
		// block docking if we're already the component docked within the
		// specified region
		if (currentlyInRegion == dockable.getComponent())
			return false;

		return true;
	}


	protected boolean isFloatable(Dockable dockable, DragOperation token) {
		// can't float null objects
		if (dockable == null || dockable.getComponent() == null
				|| token == null)
			return false;

		// can't float on a fake drag operation
		if (token.isPseudoDrag())
			return false;

		// TODO: break this check out into a separate DropPolicy class.
		// should be any customizable criteria, not hardcoded to checking
		// for being outside the bounds of a window
		if (token.isOverWindow())
			return false;

		return true;
	}


	protected DockingResults dropComponent(Dockable dockable,
	                                       DockingPort target, String region, DragOperation token) {
		if (isFloatable(dockable, token))
			return floatComponent(dockable, target, token);

		DockingResults results = new DockingResults(target, false);

		if (UNKNOWN_REGION.equals(region) || target == null) {
			return results;
		}

		Component docked = target.getDockedComponent();
		Component dockableCmp = dockable.getComponent();
		if (dockableCmp != null && dockableCmp == docked) {
			// don't allow docking the same component back into the same port
			return results;
		}

		// obtain a reference to the content pane that holds the target
		// DockingPort.
		// MUST happen before undock(), in case the undock() operation removes
		// the
		// target DockingPort from the container tree.
		Container contentPane = SwingUtility.getContentPane((Component) target);
		Point contentPaneLocation = token == null ? null : token
				.getCurrentMouse(contentPane);

		// undock the current Dockable instance from it's current parent
		// container
		undock(dockable);

		// when the original parent reevaluates its container tree after
		// undocking, it checks to see how
		// many immediate child components it has. split layouts and tabbed
		// interfaces may be managed by
		// intermediate wrapper components. When undock() is called, the docking
		// port
		// may decide that some of its intermedite wrapper components are no
		// longer needed, and it may get
		// rid of them. this isn't a hard rule, but it's possible for any given
		// DockingPort implementation.
		// In this case, the target we had resolved earlier may have been
		// removed from the component tree
		// and may no longer be valid. to be safe, we'll resolve the target
		// docking port again and see if
		// it has changed. if so, we'll adopt the resolved port as our new
		// target.
		if (contentPaneLocation != null && contentPane != null) {
			results.dropTarget = DockingUtility.findDockingPort(contentPane,
					contentPaneLocation);
			target = results.dropTarget;
		}

		results.success = target.dock(dockableCmp, region);
		SwingUtility.revalidate((Component) target);
		return results;
	}

	/**
	 * Undocks the specified {@code Dockable} from it's parent
	 * {@code DockingPort}. If {@code dockable} is {@code null} or is not
	 * currently docked within a {@code DockingPort}, then this method returns
	 * {@code false}.
	 *
	 * @param dockable the {@code Dockable} to be undocked.
	 * @return {@code true} if the undocking operation was successful,
	 * {@code false} otherwise.
	 * @see #dock(Dockable, DockingPort, String)
	 */
	public boolean undock(Dockable dockable) {
		if (dockable == null)
			return false;

		Component dragSrc = dockable.getComponent();
		Container parent = dragSrc.getParent();
		RootWindow rootWin = RootWindow.getRootContainer(parent);

		// if there's no parent container, then we really don't have anything
		// from which to to
		// undock this component, now do we?
		if (parent == null)
			return false;

		boolean success = false;
		DockingPort dockingPort = DockingUtility.getParentDockingPort(dragSrc);

		// notify that we are about to undock
		Map dragContext = DragManager.getDragContext(dockable);
		DockingEvent dockingEvent = new DockingEvent(dockable, dockingPort,
				dockingPort, DockingEvent.UNDOCKING_STARTED, dragContext);
		EventManager.dispatch(dockingEvent);
		// if(dockingEvent.isConsumed())
		// return false;

		if (dockingPort != null) {
			// if 'dragSrc' is currently docked, then undock it instead of using
			// a
			// simple remove(). this will allow the DockingPort to do any of its
			// own
			// cleanup operations associated with component removal.
			success = dockingPort.undock(dragSrc);
		} else {
			// otherwise, just remove the component
			parent.remove(dragSrc);
			success = true;
		}

		if (rootWin != null) {
			SwingUtility.revalidate(rootWin.getContentPane());
			SwingUtility.repaint(rootWin.getContentPane());
		}

		if (success) {
			dockingEvent = new DockingEvent(dockable, dockingPort, dockingPort,
					DockingEvent.UNDOCKING_COMPLETE, dragContext);
			// notify the docking port and dockable
			Object[] evtTargets = {dockingPort, dockable};
			EventManager.dispatch(dockingEvent, evtTargets);
		}

		return success;
	}


	protected DockingResults floatComponent(Dockable dockable,
	                                        DockingPort target, DragOperation token) {
		// otherwise, setup a new DockingFrame and retarget to the CENTER region
		DockingResults results = new DockingResults(target, false);

		// determine the bounds of the new frame
		Point screenLoc = token.getCurrentMouse(true);
		SwingUtility.add(screenLoc, token.getMouseOffset());
		Rectangle screenBounds = dockable.getComponent().getBounds();
		screenBounds.setLocation(screenLoc);

		// create the frame
		FloatManager mgr = DockingManager.getFloatManager();
		DockingFrame frame = mgr.floatDockable(dockable, dockable
				.getComponent(), screenBounds);

		// grab a reference to the frame's dockingPort for posterity
		results.dropTarget = frame.getDockingPort();

		results.success = true;
		return results;
	}


	protected static class DockingResults {
		public DockingResults(DockingPort port, boolean status) {
			dropTarget = port;
			success = status;
		}

		public DockingPort dropTarget;

		public boolean success;
	}


	/**
	 * Returns a new {@code DefaultDockingPort} with characteristics similar to
	 * the specified base {@code DockingPort}. If the base {@code DockingPort}
	 * is a {@code DefaultDockingPort}, then the returned {@code DockingPort}
	 * will share the base {@code DockingPort's} border manager and tabbed
	 * drag-source flag. The returned {@code DockingPort's} {@code isRoot()}
	 * method will return {@code false}.
	 *
	 * @param base the {@code DockingPort} off of which to base the returned
	 *             {@code DockingPort}
	 * @return a new {@code DefaultDockingPort} with characteristics similar to
	 * the specified base {@code DockingPort}.
	 * @see DefaultDockingPort#getBorderManager()
	 * @see DefaultDockingPort#setBorderManager(BorderManager)
	 * @see DefaultDockingPort#isTabsAsDragSource()
	 * @see DefaultDockingPort#setTabsAsDragSource(boolean)
	 * @see DefaultDockingPort#setRoot(boolean)
	 */
	public DockingPort createDockingPort(DockingPort base) {
		DockingPort port = createDockingPortImpl(base);

		if (port instanceof DefaultDockingPort
				&& base instanceof DefaultDockingPort) {
			DefaultDockingPort newPort = (DefaultDockingPort) port;
			DefaultDockingPort ddp = (DefaultDockingPort) base;
			newPort.setBorderManager(ddp.getBorderManager());
			newPort.setTabsAsDragSource(ddp.isTabsAsDragSource());
			newPort.setRoot(false);
		}
		return port;
	}

	protected DockingPort createDockingPortImpl(DockingPort base) {
		return new DefaultDockingPort();
	}


	/**
	 * Returns a new {@code DockingSplitPane} based on the specified
	 * {@code DockingPort}. and region. Creation of the
	 * {@code DockingSplitPane} is deferred to an internal protected method to
	 * allow for overriding by subclasses. A client property is set on the
	 * returned split pane with the key DockingConstants.REGION to indicate the
	 * creation region of the split pane for non-{@code DockingSplitPanes}
	 * returned by overriding subclasses.
	 * <p>
	 * This method determines the "elder" component of the split pane by
	 * checking whether the new creation region is in the TOP or LEFT
	 * (NORTH_REGION or WEST_REGION). If the creation region, representing where
	 * the new {@code Dockable} will be docked, is <b>not</b> in the top or
	 * left, then the elder {@code Component} in the split pane must be. This
	 * information is used to initialize the resize weight of the split pane,
	 * setting resize weight to {@code 1} if the elder is in the top or left of
	 * the split pane and {@code 0} if not. This gives the elder
	 * {@code Component} in the resulting split pane priority in the layout with
	 * resizing the split pane.
	 * <p>
	 * If the creation region is {@code NORTH_REGION} or {@code SOUTH_REGION},
	 * the returned split pane is initialized with a {@code VERTICAL_SPLIT}
	 * orientation; otherwise a {@code HORIZONTAL_SPLIT} orientation is used.
	 * <p>
	 * Before returning, the border is removed from the split pane, its divider
	 * size is set to 3, and if possible the border is removed from the split
	 * pane divider. This is to avoid an excessive compound border effect for
	 * embedded {@code Components} within the split pane that may have their own
	 * borders.
	 *
	 * @param base   the {@code DockingPort} off of which the returned
	 *               {@code JSplitPane} will be based.
	 * @param region the region within the base {@code DockingPort} used to
	 *               determine the orientation of the returned {@code JSplitPane}.
	 * @return a new {@code DockingSplitPane} based on the specified
	 * {@code DockingPort}. and region.
	 * @see DockingSplitPane#DockingSplitPane(DockingPort, String)
	 * @see #createSplitPaneImpl(DockingPort, String)
	 * @see JSplitPane#setResizeWeight(double)
	 */
	public JSplitPane createSplitPane(DockingPort base, String region, float percent) {
		JSplitPane split = createSplitPaneImpl(base, region);
		split.setVisible(false);
		// mark the creation region on the split pane
		SwingUtility.putClientProperty(split, DockingConstants.REGION, region);

		double resizeWeight;
		if (defaultResizeWeight == -1) {
			// the creation region represents the "new" region, not the elder
			// region.
			// so if the creation region is NOT in the top left, then the elder
			// region is.
			boolean elderInTopLeft = !DockingUtility.isRegionTopLeft(region);
			resizeWeight = elderInTopLeft ? 1 : 0;
		} else {
			resizeWeight = defaultResizeWeight;
		}

		split.setResizeWeight(resizeWeight);
		if (constantPercent && split instanceof DockingSplitPane) {
			((DockingSplitPane) split).setConstantPercent(true);
		}

		// determine the orientation
		int orientation = JSplitPane.HORIZONTAL_SPLIT;
		if (NORTH_REGION.equals(region) || SOUTH_REGION.equals(region))
			orientation = JSplitPane.VERTICAL_SPLIT;
		split.setOrientation(orientation);

		// remove the border from the split pane
		split.setBorder(null);

		// set the divider size for a more reasonable, less bulky look
		split.setDividerSize(3);
		split.setOneTouchExpandable(false);  //zw

		// check the UI. If we can't work with the UI any further, then
		// exit here.
		if (!(split.getUI() instanceof BasicSplitPaneUI))
			return split;

		// grab the divider from the UI and remove the border from it
		final BasicSplitPaneDivider divider = ((BasicSplitPaneUI) split.getUI())
				.getDivider();
		if (divider != null) {
			divider.setBorder(null);

			divider.addMouseListener(new MouseAdapter() {
				public void mouseClicked(MouseEvent e) {
					if (SwingUtilities.isLeftMouseButton(e)
							&& e.getClickCount() == 2) {
						// TODO should be not override, but placed logic here
						((JSplitPane) divider.getParent())
								.resetToPreferredSizes();
					}
				}
			});
		}

		if (percent != -1) {
			split.setDividerLocation(percent);
		}

		split.setVisible(true);

		return split;
	}

	/**
	 * Returns a new {@code DockingSplitPane} based on the specified
	 * {@code DockingPort}. and region. Creation of the
	 * {@code DockingSplitPane} is deferred to an internal protected method to
	 * allow for overriding by subclasses. A client property is set on the
	 * returned split pane with the key DockingConstants.REGION to indicate the
	 * creation region of the split pane for non-{@code DockingSplitPanes}
	 * returned by overriding subclasses.
	 * <p>
	 * This method determines the "elder" component of the split pane by
	 * checking whether the new creation region is in the TOP or LEFT
	 * (NORTH_REGION or WEST_REGION). If the creation region, representing where
	 * the new {@code Dockable} will be docked, is <b>not</b> in the top or
	 * left, then the elder {@code Component} in the split pane must be. This
	 * information is used to initialize the resize weight of the split pane,
	 * setting resize weight to {@code 1} if the elder is in the top or left of
	 * the split pane and {@code 0} if not. This gives the elder
	 * {@code Component} in the resulting split pane priority in the layout with
	 * resizing the split pane.
	 * <p>
	 * If the creation region is {@code NORTH_REGION} or {@code SOUTH_REGION},
	 * the returned split pane is initialized with a {@code VERTICAL_SPLIT}
	 * orientation; otherwise a {@code HORIZONTAL_SPLIT} orientation is used.
	 * <p>
	 * Before returning, the border is removed from the split pane, its divider
	 * size is set to 3, and if possible the border is removed from the split
	 * pane divider. This is to avoid an excessive compound border effect for
	 * embedded {@code Components} within the split pane that may have their own
	 * borders.
	 *
	 * @param base   the {@code DockingPort} off of which the returned
	 *               {@code JSplitPane} will be based.
	 * @param region the region within the base {@code DockingPort} used to
	 *               determine the orientation of the returned {@code JSplitPane}.
	 * @return a new {@code DockingSplitPane} based on the specified
	 * {@code DockingPort}. and region.
	 * @see DockingSplitPane#DockingSplitPane(DockingPort, String)
	 * @see #createSplitPaneImpl(DockingPort, String)
	 * @see JSplitPane#setResizeWeight(double)
	 */
	public JSplitPane createSplitPane(DockingPort base, String region) {
		return createSplitPane(base, region, -1f);
	}

	protected JSplitPane createSplitPaneImpl(DockingPort base, String region) {
		return new DockingSplitPane(base, region);
	}


	/**
	 * Returns the initial divider location to be used by the specified
	 * {@code JSplitPane} when it is embedded within the specified
	 * {@code DockingPort}. It is assumed that the {@code JSplitPane} parameter
	 * is embedded within the specified {@code DockingPort}, is validated,
	 * visible, and its dimensions are non-zero.
	 * <p>
	 * This method gets the "size" of the specified {@code DockingPort} based on
	 * the orientation of the split pane (<i>width</i> for horizontal split,
	 * <i>height</i> for vertical split) minus the {@code DockingPort's}
	 * insets. It then dispatches to
	 * {@code getDividerProportion(DockingPort port, JSplitPane splitPane)} to
	 * determine the preferred proportion of the split pane divider. The
	 * returned value for this method is the product of the {@code DockingPort}
	 * size and the split proportion.
	 * <p>
	 * If either {@code port} or {@code splitPane} parameters are {@code null},
	 * then this method returns {@code 0}.
	 *
	 * @param port      the {@code DockingPort} that contains the specified
	 *                  {@code JSplitPane}.
	 * @param splitPane the {@code JSplitPane} whose initial divider location is to be
	 *                  determined.
	 * @return the desired divider location of the supplied {@code JSplitPane}.
	 * @see DockingStrategy#getInitialDividerLocation(DockingPort, JSplitPane)
	 * @see #getDividerProportion(DockingPort, JSplitPane)
	 */
	public int getInitialDividerLocation(DockingPort port, JSplitPane splitPane) {
		if (port == null || splitPane == null)
			return 0;

		Container dockingPort = (Container) port;
		Insets in = dockingPort.getInsets();
		boolean vert = splitPane.getOrientation() == JSplitPane.VERTICAL_SPLIT;
		int inset = vert ? in.top + in.bottom : in.left + in.right;

		// get the dimensions of the DockingPort, minus the insets
		int portSize = vert ? dockingPort.getHeight() : dockingPort.getWidth();
		portSize -= inset;

		// get the divider proportion for the split pane and multiply by the
		// port size
		double proportion = getDividerProportion(port, splitPane);
		if (proportion < 0 || proportion > 1)
			proportion = 0.5d;

		return (int) (portSize * proportion);
	}

	/**
	 * Returns the desired divider proportion of the specified
	 * {@code JSplitPane} after rendering. This method assumes that the
	 * {@code JSplitPane} parameter is, or will be embedded within the specified
	 * {@code DockingPort}. This method does <b>not</b> assume that the
	 * {@code JSplitPane} has been validated and that it's current dimensions
	 * are non-zero.
	 * <p>
	 * If either {@code port} or {@code splitPane} parameters are {@code null},
	 * then this method returns the default value of
	 * {@code RegionChecker.DEFAULT_SIBLING_SIZE}. Otherwise the "elder"
	 * component within the {@code JSplitPane} is determined to see if it is
	 * contained within a sub-{@code DockingPort}. If the "elder"
	 * {@code Component} cannot be determined, or it is not contained within a
	 * sub-{@code DockingPort}, then the default value of
	 * {@code RegionChecker.DEFAULT_SIBLING_SIZE} is returned.
	 * <p>
	 * If the "elder" {@code Component} is successfully resolved inside a sub-{@code DockingPort},
	 * then a check is done on the sub-port for the client property
	 * {@code DefaultDockingStrategy.PREFERRED_PROPORTION}. If this value is
	 * found, then the primitive float version of it is returned.
	 * <p>
	 * Failing these checks, the {@code Dockable} is resolved for the "elder"
	 * {@code Component} in the specified {@code JSplitPane} via
	 * {@code DockingManager.getDockable(Component comp)}. If no
	 * {@code Dockable} can be found, then
	 * {@code RegionChecker.DEFAULT_SIBLING_SIZE} is returned.
	 * <p>
	 * Otherwise, the {@code DockingPortPropertySet} is retrieved from the
	 * specified {@code DockingPort} and its {@code getRegionChecker()} method
	 * is called. {@code getSiblingSize(Component c, String region)} is invoked
	 * on the returned {@code RegionChecker} passing the "elder"
	 * {@code Component} in the split pane and the creation region resolved for
	 * the specified {@code JSplitPane}. This resolves the preferred sibling
	 * size for the elder {@code Dockable} component. If the elder
	 * {@code Component} is in the top/left of the split pane, then
	 * {@code 1F-prefSize} is returned. Otherwise, the preferred sibling size is
	 * returned.
	 *
	 * @param port      the {@code DockingPort} that contains, or will contain the
	 *                  specified {@code JSplitPane}.
	 * @param splitPane the {@code JSplitPane} whose initial divider location is to be
	 *                  determined.
	 * @return the desired divider proportion of the supplied {@code JSplitPane}.
	 * @see RegionChecker#DEFAULT_SIBLING_SIZE
	 * @see #PREFERRED_PROPORTION
	 * @see DockingManager#getDockable(Component)
	 * @see RegionChecker#getSiblingSize(Component, String)
	 */
	public double getDividerProportion(DockingPort port, JSplitPane splitPane) {
		if (port == null || splitPane == null)
			return DockingManager.getDefaultSiblingSize();

		Component elder = getElderComponent(splitPane);
		if (elder == null)
			return DockingManager.getDefaultSiblingSize();

		Float prefProp = getPreferredProportion(splitPane, elder);
		if (prefProp != null)
			return prefProp.doubleValue();

		if (elder instanceof DockingSplitPane) {
			elder = ((DockingSplitPane) elder).getElderComponent();
		}

		Dockable dockable = DockingManager.getDockable(elder);
		if (dockable != null) {
			// DockingSplitPane splitter = (DockingSplitPane)splitPane;
			RegionChecker rc = port.getDockingProperties().getRegionChecker();
			float prefSize = rc.getSiblingSize(dockable.getComponent(),
					getCreationRegion(splitPane));
			return isElderTopLeft(splitPane) ? 1f - prefSize : prefSize;
			// return prefSize;
		}

		return DockingManager.getDefaultSiblingSize();
	}

	protected String getCreationRegion(JSplitPane splitPane) {
		if (splitPane instanceof DockingSplitPane)
			return ((DockingSplitPane) splitPane).getRegion();
		return (String) SwingUtility.getClientProperty(splitPane,
				DockingConstants.REGION);
	}

	protected boolean isElderTopLeft(JSplitPane splitPane) {
		if (splitPane instanceof DockingSplitPane)
			return ((DockingSplitPane) splitPane).isElderTopLeft();
		String region = getCreationRegion(splitPane);
		// creation region represents the "new" region, not the "elder" region.
		// so if the "new" region is NOT the topLeft, then the "elder" is.
		return !DockingUtility.isRegionTopLeft(region);
	}

	protected Float getPreferredProportion(JSplitPane splitPane,
	                                       Component controller) {
		// 'controller' is inside a dockingPort. re-reference to the parent
		// dockingPort.
		Container controllerPort = controller.getParent();
		return getPreferredProportion(controllerPort);
	}

	protected Component getElderComponent(JSplitPane splitPane) {
		if (splitPane instanceof DockingSplitPane)
			return ((DockingSplitPane) splitPane).getElderComponent();

		boolean inTopLeft = isElderTopLeft(splitPane);
		Component comp = inTopLeft ? splitPane.getLeftComponent() : splitPane
				.getRightComponent();
		if (comp instanceof DockingPort)
			comp = ((DockingPort) comp).getDockedComponent();
		return comp;
	}


	protected static Float getPreferredProportion(Component c) {
		return c == null ? null : (Float) SwingUtility.getClientProperty(c,
				PREFERRED_PROPORTION);
	}

	public static void setDefaultResizeWeight(double rw) {
		defaultResizeWeight = rw;
	}

	public static void keepConstantPercentage(boolean cstPercent) {
		constantPercent = cstPercent;
	}
}
